package engine.individuals;

import its.model.environment.crossroad.Program;
import its.model.environment.crossroad.ProgramPhase;
import its.opt.program.ProgramBuilder;
import its.util.RandomFactory;
import its.util.TimeUtils;

import java.util.Arrays;
import java.util.Iterator;
import java.util.logging.Level;
import java.util.logging.Logger;

public class NaturalTrafficNetIndividual implements
		TrafficNetIndividual<Integer>, Cloneable {

	/** Logger fo this class. */
	private static final Logger LOGGER = Logger
			.getLogger(NaturalTrafficNetIndividual.class.getName());

//	public static final int MAX_PROGRAM_LENGTH = 79; // 63 to select + 4 *
	// MIN_PHASE_LENGTH
	public static final int MIN_PHASE_LENGTH = ProgramBuilder.MIN_PHASE_LENGTH;
	public static final int MAX_PHASE_LENGTH = ProgramBuilder.MAX_PHASE_LENGTH;
	private Program[] programs;

	/**
	 * Cache for number of phases. <b>Do not change the current programs</b> (
	 * {@link NaturalTrafficNetIndividual#programs})
	 */
	private int phasesCount;

	public NaturalTrafficNetIndividual(Program[] programs) {
		this.programs = programs;
		this.phasesCount = 0;
	}

	// public int getProgramsSize() {
	// return programs.length + countPhasesForPrograms();
	// }

	/**
	 * Phases lengths are decreased by
	 * {@link NaturalTrafficNetIndividual#MIN_PHASE_LENGTH} before returning a
	 * bit.
	 */
	@Override
	public Integer get(int index) {
		if (index < programs.length - 1) {
			return TimeUtils.convertMillisToSeconds(programs[index].getPhaseShift());
		} else {
			// int checkedInts = programs.length;
			return getImpl(index) - MIN_PHASE_LENGTH;
		}
	}

	private Integer getImpl(int index) {
		int checkedInts = programs.length - 1;

		for (int i = 0; i < programs.length; ++i) {
			Program program = programs[i];
			int noPhases = program.getPhasesCount();

			if (index < checkedInts + noPhases) {
				ProgramPhase phase = program.getPhase(index - checkedInts);

				return TimeUtils.convertMillisToSeconds(phase.getDuration());
			}

			checkedInts += noPhases;
		}

		return null;
	}

	// private int getShift(int whichShift) {
	// return
	// TimeUtils.convertMillisToSeconds(programs[whichShift].getPhaseShift());
	// }

	@Override
	public void set(int index, Integer valueInSeconds) {
		if (index < programs.length - 1) {
			programs[index].setPhaseShift(
					TimeUtils.convertSecondsToMillis(valueInSeconds)
					% programs[index].getProgramLength());
		} else {

			// if (valueInSeconds < MIN_PHASE_LENGTH) {
			// valueInSeconds = MIN_PHASE_LENGTH;
			// } else if (valueInSeconds > MAX_PROGRAM_LENGTH) {
			// valueInSeconds = MAX_PROGRAM_LENGTH;
			// }

			// int checkedInts = programs.length;
			setImpl(index, valueInSeconds + MIN_PHASE_LENGTH);
		}
	}

	private void setImpl(int index, Integer valueInSeconds) {
		int checkedInts = programs.length - 1;

		for (int i = 0; i < programs.length; ++i) {
			Program program = programs[i];
			int noPhases = program.getPhasesCount();

			if (index < checkedInts + noPhases) {
				ProgramPhase phase = program.getPhase(index - checkedInts);

				// int currentPhaseDuration =
				// TimeUtils.convertMillisToSeconds(phase.getDuration());
				//					
				// int changeOfLastPhaseDuration = computeChangeOfPhaseDuration(
				// currentPhaseDuration,
				// valueInSeconds);
				//					
				// int lastPhaseDurationAfterChange =
				// getLastPhaseDurationInSecondsAfterChange(
				// program,
				// changeOfLastPhaseDuration);

				phase.setDuration(TimeUtils.convertSecondsToMillis(valueInSeconds));

				// program.getLastPhase().setDuration(TimeUtils.convertSecondsToMillis(lastPhaseDurationAfterChange));

				break; // done
			}

			checkedInts += noPhases;
		}
	}

	// private int getLastPhaseDurationInSecondsAfterChange(
	// Program program,
	// int changeBy)
	// {
	// return
	// TimeUtils.convertMillisToSeconds(program.getLastPhase().getDuration()) +
	// changeBy;
	// }
	//
	// private int computeChangeOfPhaseDuration(
	// int currentDuration,
	// int switched)
	// {
	// return currentDuration - switched;
	// }

	// public void setShiftAtIndexToValueInSeconds(int index, Integer seconds)
	// {
	// setPropperProgramLength(index);
	// programs[index].setPhaseShift(TimeUtils.convertSecondsToMillis(seconds));
	// }

	// private void setPropperProgramLength(int index) {
	// programs[index].setProgramLength(TimeUtils.convertSecondsToMillis(MAX_PROGRAM_LENGTH));
	// }

	/**
	 * Number of bits in programs coding in the traffic net.
	 */
	@Override
	public int size() {
		return countPhasesForPrograms() + programs.length - 1;
	}

	private int countPhasesForPrograms() {
		int pC = phasesCount;

		if (pC == 0) {
			for (Program program : programs) {
				pC += program.getPhasesCount();
			}
			phasesCount = pC;
		}

		return pC;
	}

	// private int countPhasesShifts() {
	// return programs.length - 1; // excluding the last one
	// }

	/*
	 * (non-Javadoc)
	 * 
	 * @see java.lang.Object#toString()
	 */
	@Override
	public String toString() {
		return Arrays.toString(programs);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see java.lang.Object#equals(java.lang.Object)
	 */
	@Override
	public boolean equals(Object other) {
		if (this == other) {
			return true;
		}

		if (!(other instanceof NaturalTrafficNetIndividual)) {
			return false;
		}

		NaturalTrafficNetIndividual otherA = (NaturalTrafficNetIndividual) other;

		return Arrays.equals(programs, otherA.programs);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see java.lang.Object#hashCode()
	 */
	@Override
	public int hashCode() {
		return Arrays.hashCode(programs);
	}

	/**
	 * @return the programs
	 */
	public Program[] getPrograms() {
		return programs;
	}

	public boolean isFeasibleSolution() {
		for (Program program : programs) {
			if (!isFeasibleProgram(program)) {
				return false;
			}
		}

		return true;
	}

	private boolean isFeasibleProgram(Program program) {
		if (!isFeasibleShift(program, TimeUtils.convertMillisToSeconds(program
				.getPhaseShift()))) {
			return false;
		}

		int programLength = 0;

		Iterator<ProgramPhase> phaseIterator = program.getPhaseIterator();
		while (phaseIterator.hasNext()) {
			ProgramPhase programPhase = phaseIterator.next();

			int phaseLength = TimeUtils.convertMillisToSeconds(programPhase
					.getDuration());

			if (!isFeasiblePhaseLength(phaseLength)) {
				return false;
			}

			programLength += phaseLength;
		}

		if (programLength != TimeUtils.convertMillisToSeconds(program
				.getProgramLength())) {
			return false;
		}

		return true;
	}

	private boolean isFeasibleShift(Program p, Integer seconds) {
		return seconds >= 0
				&& seconds <= TimeUtils.convertMillisToSeconds(p.getProgramLength());
	}

	private boolean isFeasiblePhaseLength(int phaseDurationSeconds) {
		return phaseDurationSeconds >= MIN_PHASE_LENGTH
				&& phaseDurationSeconds <= MAX_PHASE_LENGTH;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see java.lang.Object#clone()
	 */
	@Override
	public NaturalTrafficNetIndividual clone() throws CloneNotSupportedException {
		NaturalTrafficNetIndividual clone = (NaturalTrafficNetIndividual) super
				.clone();

		clone.programs = new Program[programs.length];

		for (int i = 0; i < programs.length; ++i) {
			clone.programs[i] = programs[i].clone();
			clone.programs[i].setProgramLength(TimeUtils.convertSecondsToMillis(getMaxProgramLength(programs[i])));
		}
		return clone;
	}

	/**
	 * Realizes uniform cross over for two individuals.
	 * 
	 * @param ind
	 *          not null individual to crossover with current
	 * @return Array of two not null crossed individuals
	 */
	@Override
	public TrafficNetIndividual[] crossOver(TrafficNetIndividual individual) {
		assert individual != null : "Crossed individual cannot be empty!";

		NaturalTrafficNetIndividual ind = (NaturalTrafficNetIndividual) individual;

		NaturalTrafficNetIndividual[] crossed = new NaturalTrafficNetIndividual[2];

		NaturalTrafficNetIndividual crossed1 = null;
		NaturalTrafficNetIndividual crossed2 = null;
		try {
			crossed1 = this.clone();
			crossed2 = ind.clone();
		} catch (CloneNotSupportedException e) {
			LOGGER.severe("Couldn't clone individual.\n" + e);
			throw new IllegalStateException(e);
		}

		double randRatio = RandomFactory.getNextDouble();
		for (int i = 0; i < crossed1.size(); i++) {

			int c1Value = crossed1.get(i).intValue();
			int c2Value = crossed2.get(i).intValue();

			int floorCeil = (int) Math
					.floor(randRatio * (double) (c2Value - c1Value));
			int z1 = c1Value + floorCeil;
			int z2 = c2Value + c1Value - z1;

			crossed1.set(i, z1);
			crossed2.set(i, z2);
		}

		crossed1.heal();
		crossed2.heal();

		crossed[0] = crossed1;
		crossed[1] = crossed2;

		return crossed;
	}

	/**
	 * Mutates random progams net variable.
	 */
	public void mutate() {

		if (RandomFactory.getNextBoolean() 
				&& programs.length > 1) { // no use to shift using one program
			mutatePhaseShift();
			return;
		}
		
		int indexOrder = RandomFactory.getNextInt(nonZeroAlleleCount());
		int realIndex = findRealNonZeroValuedIndex(indexOrder);
		int section = findProgramFor(realIndex);
		
		if (section == -1) {
			return;
		}
		
		int offset = getOffsetForProgram(section);

		int alleleValue = get(realIndex);

		int valueToDistribute = RandomFactory.getNextInt(alleleValue) + 1;

		set(realIndex, alleleValue - valueToDistribute);

		while (valueToDistribute > 0) {

			int randomIndex;
			do {
				randomIndex = RandomFactory.getNextInt(programs[section]
						.getPhasesCount())
						+ offset;
			} while (randomIndex == realIndex);

			set(randomIndex, get(randomIndex) + 1);

			--valueToDistribute;
		}
	}

	private void mutatePhaseShift() {
		int programIndex = RandomFactory.getNextInt(programs.length - 1);
		int randomShift = RandomFactory.getNextInt(TimeUtils
				.convertMillisToSeconds(programs[programIndex].getProgramLength()));
		set(programIndex, randomShift);
	}

	private int getOffsetForProgram(int index) {
		int result = programs.length - 1;
		for (int i = 0; i < index; ++i) {
			result += programs[i].getPhasesCount();
		}
		return result;
	}

	private int findProgramFor(int index) {
		int i = 0;
		int c = programs.length - 1;
		
		if (index < c) {
			// you hit phase shift
			return -1;
		}
		
		for (Program program : programs) {

			c += program.getPhasesCount();
			if (index < c) {
				break;
			}

			++i;
		}
		return i;
	}

	private int findRealNonZeroValuedIndex(int indexOrder) {
		int realIndex = programs.length - 1;

		for (int i = programs.length - 1; i < size(); ++i) {
			if (get(i) > 0 && indexOrder >= 0) {
				realIndex = i;
				if (indexOrder-- == 0) {
					break;
				}
			}
		}
		return realIndex;
	}

	private int nonZeroAlleleCount() {

		int result = 0;
		for (int i = programs.length - 1; i < size(); ++i) {
			if (get(i) > 0) {
				++result;
			}
		}

		return result;
	}

	@Override
	public void heal() {

		if (!isFeasibleSolution()) {

			int programIndex = 0;
			for (Program p : programs) {

				int sum = sum(p);
				int programLength = TimeUtils.convertMillisToSeconds(p
						.getProgramLength());
				
				if (sum != programLength) {
					int fixValue = programLength - sum;
					int fixDiff = fixValue;
					int pC = p.getPhasesCount();
					int maxPhaseLen = programLength - pC * MIN_PHASE_LENGTH;
					int offset = getOffsetForProgram(programIndex);

					int i = pC - 1;
					while (i >= 0) {
						int fixRatio = (int) ((double) fixValue * (double) (get(i + offset) / (double) (sum - pC
								* MIN_PHASE_LENGTH)));
						if (fixRatio != 0 && get(i + offset) + fixRatio <= maxPhaseLen
								&& get(i + offset) + fixRatio >= 0) {
							fixDiff -= fixRatio;
							set(i + offset, get(i + offset) + fixRatio);
							if (fixDiff == 0) {
								break;
							}
						}
						--i;
					}
					if (fixDiff != 0) {
						if (fixDiff > 0) {
							i = pC - 1;
							while (i >= 0) {
								if (get(i + offset) + fixDiff <= maxPhaseLen) {
									set(i + offset, get(i + offset) + fixDiff);
									break;
								}
								--i;
							}
						} else if (fixDiff < 0) {
							i = pC - 1;
							while (i >= 0) {
								if (get(i + offset) + fixDiff >= 0) {
									set(i + offset, get(i + offset) + fixDiff);
									break;
								}
								--i;
							}
						}
					}
					if (sum(p) != programLength) {
						IllegalStateException e = new IllegalStateException(
								"Inconsistent state. Sum is: " + sum(p) + "for individual: "
										+ this);
						LOGGER.log(Level.SEVERE, e.getMessage(), e);
						throw e;
					}
				}
				++programIndex;
			}
		}
		if (!isFeasibleSolution()) {
			IllegalStateException e = new IllegalStateException(
					"Inconsistent state of solution: " + this);
			LOGGER.log(Level.SEVERE, e.getMessage(), e);
			throw e;
		}
	}

	private int sum(Program p) {
		int result = 0;
		Iterator<ProgramPhase> iter = p.getPhaseIterator();
		while (iter.hasNext()) {
			ProgramPhase phase = (ProgramPhase) iter.next();
			result += TimeUtils.convertMillisToSeconds(phase.getDuration());
		}
		return result;
	}
	
	/**
	 * <code>(p.getPhasesCount() - 1) * MIN_PHASE_LENGTH + MAX_PHASE_LENGTH</code>
	 * 
	 * @param p
	 * @return
	 *         <code>(p.getPhasesCount() - 1) * MIN_PHASE_LENGTH + MAX_PHASE_LENGTH</code>
	 */
	private int getMaxProgramLength(Program p) {
		return (p.getPhasesCount() - 1) * MIN_PHASE_LENGTH + MAX_PHASE_LENGTH;
	}
}
